Replace httpx and httpx-sse with httpx2#2972
Conversation
There was a problem hiding this comment.
5 issues found across 65 files
Tip: instead of fixing issues one by one fix them all with cubic
Partial review: This PR has more than 50 files, so cubic reviewed the highest-priority files first. During the trial, paid plans get a higher file limit.
You can try an ultrareview to bypass the file limit, comment @cubic-dev-ai ultrareview. Learn more.
Re-trigger cubic
| # stderr (agronholm/anyio#816, fixed in 4.10). | ||
| "anyio>=4.10; python_version >= '3.14'", | ||
| "anyio>=4.9; python_version < '3.14'", | ||
| "httpx>=0.27.1,<1.0.0", | ||
| "httpx-sse>=0.4", | ||
| "httpx2>=2.5.0", | ||
| "pydantic>=2.12.0", | ||
| "starlette>=0.48.0; python_version >= '3.14'", | ||
| "starlette>=0.27; python_version < '3.14'", |
There was a problem hiding this comment.
🔴 Making the just-published httpx2/httpcore2 packages (uploaded to PyPI ~45 minutes before this PR was opened, per the lockfile timestamps) the SDK's sole HTTP dependency deserves explicit provenance/maturity vetting before merge, and the swap also silently changes TLS verification: httpcore2/httpx2 drop certifi for truststore, so certificate validation moves from the certifi CA bundle to the OS trust store for every SDK user. At minimum, document the certifi→truststore TLS behaviour change in docs/migration.md (which currently only covers the import rename) and confirm the fork's publisher before pinning the SDK to an hours-old 2.5.0 release.
Extended reasoning...
What the change does. The PR replaces httpx>=0.27.1,<1.0.0 + httpx-sse>=0.4 with httpx2>=2.5.0 as the SDK's only HTTP stack (pyproject.toml lines 30–36), and the regenerated uv.lock shows the transport swap underneath it: httpcore (which depends on certifi + h11) is replaced by httpcore2 (which depends on h11 + truststore), and httpx2 itself also pulls in truststore instead of certifi.
Maturity/provenance concern. The lockfile records the upload times of the new packages: httpcore2-2.5.0 at 2026-06-25T14:16:53–56Z and httpx2-2.5.0 at 2026-06-25T14:16:55–57Z, while this PR was opened at 2026-06-25T15:03:08Z. In other words, the SDK would pin its entire HTTP/SSE/OAuth transport to packages that were published to PyPI less than an hour before the PR — zero deployment track record, no ecosystem usage, and the PR description asserts "next-generation httpx fork" without substantiating who publishes it. The wheel metadata does list Tom Christie as author and Pydantic Services Inc. as maintainer with a github.com/pydantic/httpx2 homepage, which softens the worst-case typosquat scenario, but that metadata is self-declared and should be verified by maintainers (confirm the PyPI publisher, the GitHub org, and that this is the intended successor to httpx) before the SDK adopts it as its sole HTTP dependency.
The undocumented TLS behaviour change. This is the independently actionable part regardless of how the provenance question resolves. With httpx <1.0, default TLS verification used the certifi CA bundle. httpx2 2.5.0's create_ssl_context() instead defaults to truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) — the operating system trust store — unless SSL_CERT_FILE/SSL_CERT_DIR are set. Every SDK user's HTTP, SSE, and OAuth requests therefore start validating server certificates against a different trust root the moment they upgrade.
Concrete walk-through of how this manifests. Take a user running an MCP client in a corporate environment behind a TLS-intercepting proxy whose root CA is installed in the OS trust store but (deliberately) not in certifi. On v1, streamable_http_client(...) → create_mcp_http_client() → httpx.AsyncClient() → certifi bundle → the proxy's certificate is rejected with SSLCertVerificationError (the behaviour they have built tooling/expectations around, e.g. mounting a custom bundle via SSL_CERT_FILE). After this PR, the same call path goes httpx2.AsyncClient() → truststore → OS store → the connection now succeeds. The inverse failure also exists: a minimal container image (e.g. python:slim derivatives without ca-certificates installed, or distroless variants) that previously worked because certifi ships its own bundle will now fail every TLS handshake because the OS store is empty. Neither direction is hinted at anywhere — docs/migration.md documents only the httpx → httpx2 import rename and the SSE helper change.
Why nothing else in the PR mitigates this. The migration guide explicitly claims "httpx2 is API-compatible with httpx, so usually only the import name changes", which actively tells users there is no behavioural change to look for. The test suite runs entirely against in-process ASGI transports and MockTransport, so no test exercises real TLS and the change is invisible in CI.
How to fix. (1) Add a section to docs/migration.md (and ideally docs/installation.md) stating that TLS verification now uses the system trust store via truststore instead of the certifi bundle, and how to restore the old behaviour (set SSL_CERT_FILE to certifi's bundle, or pass an explicit verify= SSL context to a user-supplied httpx2.AsyncClient). (2) Before merging, have a maintainer verify the httpx2/httpcore2 PyPI publisher and decide whether pinning to a release that is hours old (>=2.5.0 with no upper bound) is acceptable for the SDK, or whether to wait for the fork to accumulate a track record / add a stricter version constraint.
There was a problem hiding this comment.
✅ The documentation half of this is now addressed: commit c5d54b2 added a section to docs/migration.md explaining that TLS verification moves from the bundled certifi CA list to the OS trust store via truststore, including the minimal-container failure mode and the verify=ssl_context escape hatch.
Still outstanding is the provenance/maturity half: pyproject.toml continues to pin httpx2>=2.5.0 (no upper bound) — a release that was published to PyPI less than an hour before this PR was opened — and uv.lock resolves the transport stack to the equally new httpcore2 2.5.0. Before merge, a maintainer should (1) confirm the httpx2/httpcore2 PyPI publisher and the github.com/pydantic/httpx2 org match the intended httpx successor, and (2) decide whether an unbounded >=2.5.0 constraint on an hours-old fork is acceptable for the SDK's sole HTTP dependency, or whether a tighter cap (e.g. <3) / waiting for a track record is warranted. No further code change is needed for the TLS documentation point.
|
|
httpx2 (2.5.0) is the next-generation httpx fork with server-sent events support built in, so the separate httpx-sse dependency is no longer needed. - Swap the httpx/httpx-sse dependencies for httpx2>=2.5.0 in the SDK and the example projects. - Rewrite the SSE transports against httpx2's API: aconnect_sse(...) -> client.stream(...)/client.sse(...) wrapped in EventSource, and iterate the EventSource directly instead of .aiter_sse(). - Document the swap as a v2 breaking change in docs/migration.md and update docs/installation.md, README.v2.md, and the example sources. Verified: ruff, pyright, and the full test suite pass at 100% coverage.
The rebase onto main picked up files added since the swap (stories examples, identity-assertion client/docs, client probe, docs_src tutorials) that still imported httpx. Apply the same httpx -> httpx2 rename to them, update the migration guide's mcp-types and identity-assertion sections to name httpx2, and document the certifi -> truststore TLS verification change.
httpx-sse's aconnect_sse() always sent Accept: text/event-stream and Cache-Control: no-store; the swap to bare client.stream() dropped both. Open the legacy SSE GET and the streamable HTTP GET/resumption/ reconnection streams with AsyncClient.sse(), which injects those headers (explicit caller headers still take precedence), and update the mocked sse_client test to drive the new call. Example fixes from the same review pass: - simple-chatbot: catch httpx2.HTTPError instead of RequestError so raise_for_status() failures take the handled path (the HTTPStatusError isinstance branch was unreachable), and drop the Raises section the method never honoured. - sse-polling-client: suppress the httpcore2 logger; httpcore is no longer in the dependency tree, so the old suppression was a no-op.
86e257f to
d4b5611
Compare
httpx2's ServerSentEvent declares id as str defaulting to empty, where httpx-sse allowed str | None. The handler under test checks truthiness, so the default is behaviourally identical.
Summary
Swaps the
httpx+httpx-ssedependencies forhttpx2>=2.5.0.httpx2is the next-generation httpx fork with server-sent events support built in, so the separatehttpx-ssedependency is no longer needed.Changes
httpx>=0.27.1,<1.0.0+httpx-sse>=0.4withhttpx2>=2.5.0in the SDK and the example projects (lockfile regenerated).aconnect_sse(...)→client.stream(...)/client.sse(...)wrapped inEventSource, and iterate theEventSourcedirectly instead of.aiter_sse().docs/migration.md; updatedocs/installation.md,README.v2.md, and the example sources.Notes
httpx2is API-compatible withhttpx, so most of the diff is thehttpx→httpx2import rename. Users passing their ownhttp_clientonly need to change the import.AsyncClient.sse()convenience method for the raw-client SSE call sites.Verification
ruff,pyright, and the full test suite all pass at 100% coverage (strict-no-coverclean).AI Disclaimer
This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.